package ua.kharkov.khpi.tarasenko.bach.service;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.servlet.ServletContext;

import org.apache.log4j.Logger;

import ua.kharkov.khpi.tarasenko.bach.analysis.prefixspan.Item;
import ua.kharkov.khpi.tarasenko.bach.analysis.prefixspan.Node;
import ua.kharkov.khpi.tarasenko.bach.analysis.prefixspan.RulesGenerator;
import ua.kharkov.khpi.tarasenko.bach.analysis.prefixspan.Sequence;
import ua.kharkov.khpi.tarasenko.bach.core.Constant;
import ua.kharkov.khpi.tarasenko.bach.core.EShopException;
import ua.kharkov.khpi.tarasenko.bach.core.db.ConnectionManager;
import ua.kharkov.khpi.tarasenko.bach.core.db.DBUtils;
import ua.kharkov.khpi.tarasenko.bach.core.item.ItemBean;
import ua.kharkov.khpi.tarasenko.bach.core.logging.TransactionType;
import ua.kharkov.khpi.tarasenko.bach.core.user.ShoppingCart;
import ua.kharkov.khpi.tarasenko.bach.core.utils.ProductUtils;
import ua.kharkov.khpi.tarasenko.bach.core.utils.RuleUtils;
import ua.kharkov.khpi.tarasenko.bach.dao.core.DAOFactory;
import ua.kharkov.khpi.tarasenko.bach.dao.core.DAOFactory.DAO_TYPE;
import ua.kharkov.khpi.tarasenko.bach.dao.core.assocrules.AssocRulesDAO;
import ua.kharkov.khpi.tarasenko.bach.dao.core.item.ItemDAO;
import ua.kharkov.khpi.tarasenko.bach.dao.core.item.ItemDTO;
import ua.kharkov.khpi.tarasenko.bach.dao.mysql.assocrules.AssocRulesDAOImpl;
import ua.kharkov.khpi.tarasenko.bach.web.ui_beans.RecommendationBean;

public class RecommendationService {
	private static final Logger LOG = Logger
			.getLogger(RecommendationService.class);

	public static int NUMBER_OF_RECOMMENDATIONS = 3;

	private ServletContext context;

	public RecommendationService(ServletContext context) {
		this.context = context;
	}

	public RecommendationBean getRecommendations(List<Integer> accessSequence,
			ShoppingCart cart, String type) {
		RecommendationBean recBean = new RecommendationBean();

		try {
			if (type.equals("cart")) {
				recBean = getCartRecommendations(cart);
			} else {
				recBean = getNavRecommendations(accessSequence);
			}
		} catch (EShopException e) {
			LOG.error("Error occurred during getting recommendations", e);
		}
		if (recBean.getSize() < NUMBER_OF_RECOMMENDATIONS) {
			try {
				recBean.join(getTopPopularProduct(recBean, cart,
						NUMBER_OF_RECOMMENDATIONS - recBean.getSize()));
			} catch (EShopException e) {
				LOG.error(
						"Error occurred during getting top products recommendations",
						e);
			}
		}

		return recBean;
	}

	private RecommendationBean getNavRecommendations(
			List<Integer> accessSequence) throws EShopException {
		RecommendationBean recBean = new RecommendationBean();

		Object recTreeObj = context
				.getAttribute(Constant.CONTEXT_ATTRIBUTE_TREE);
		if (recTreeObj != null) {
			Node recTree = (Node) recTreeObj;
			RulesGenerator rg = new RulesGenerator(recTree, 1, 8,
					NUMBER_OF_RECOMMENDATIONS);

			Sequence seq = new Sequence();
			for (Integer itemId : accessSequence) {
				Item item = Item.getItem(itemId.toString());
				item.setTransactionType(TransactionType.VIEW);
				seq.addItem(item);
			}
			Set<Item> recsSet = rg.getRecommendation(seq);
			LOG.trace("Got navRecs: " + Arrays.toString(recsSet.toArray()));
			
			Connection conn = ConnectionManager.getConnection();
			ItemDAO itemDAO = DAOFactory.getDAOFactory(DAO_TYPE.MYSQL)
					.getItemDAO(conn);

			for (Item item : recsSet) {
				ItemDTO itemDTO = itemDAO.findItemById(Integer.valueOf(item
						.getItem()));
				ItemBean itemBean = ProductUtils.transformDtoToBean(itemDTO);
				recBean.addItem(itemBean);
			}
		} else {
			LOG.error("There is no recommendation tree in DB");
		}

		return recBean;
	}

	private RecommendationBean getCartRecommendations(ShoppingCart cart)
			throws EShopException {
		RecommendationBean recBean = new RecommendationBean();

		Integer[] itemsId = generateDontForgetRecommendations(
				cart.getItemsIdi(), NUMBER_OF_RECOMMENDATIONS);
		LOG.trace("Got cartRecs: " + Arrays.toString(itemsId));
		Connection conn = ConnectionManager.getConnection();
		ItemDAO itemDAO = DAOFactory.getDAOFactory(DAO_TYPE.MYSQL).getItemDAO(
				conn);
		for (Integer itemId : itemsId) {
			if (itemId != null) {
				ItemDTO itemDTO = itemDAO.findItemById(itemId);
				ItemBean itemBean = ProductUtils.transformDtoToBean(itemDTO);
				recBean.addItem(itemBean);
			}
		}
		DBUtils.close(conn);
		return recBean;
	}

	private RecommendationBean getTopPopularProduct(RecommendationBean rb,
			ShoppingCart cart, int number) throws EShopException {

		Integer[] prodInCartIds = cart.getItemsIdi();

		Connection conn = null;
		List<ItemBean> itemsBean = Collections.emptyList();
		try {
			conn = ConnectionManager.getConnection();
			ItemDAO itemDAO = DAOFactory.getDAOFactory(
					DAOFactory.DAO_TYPE.MYSQL).getItemDAO(conn);

			List<ItemDTO> itemsDTO = itemDAO.getTopNAvailableProducts(number,
					prodInCartIds);

			// Ordered list. First - the most popular.
			itemsBean = new ArrayList<ItemBean>(number);
			for (ItemDTO item : itemsDTO) {
				itemsBean.add(ProductUtils.transformDtoToBean(item));
			}
		} catch (EShopException e) {
			DBUtils.rollBackAndClose(conn);
			throw e;
		} finally {
			if (conn != null) {
				DBUtils.close(conn);
			}
		}

		RecommendationBean recBean = new RecommendationBean();
		recBean.addAll(itemsBean);

		return recBean;
	}

	/**
	 * 
	 * 
	 * @param itemsInCart
	 *            - ids should be sorted in ASC order!
	 * @param num
	 * @return
	 * @throws EShopException
	 */
	private Integer[] generateDontForgetRecommendations(Integer[] itemsInCart,
			int num) throws EShopException {

		Connection conn = ConnectionManager.getConnection();

		// Get possible combinations of items from the cart in case there is no
		// rules(or number of items in Consequent is less than "num"
		List<Integer[]> itemSets = new ArrayList<Integer[]>();
		for (int i = 0; i < itemsInCart.length; i++) {
			Integer[] curr = new Integer[1];
			curr[0] = itemsInCart[i];
			Integer[] another = Arrays.copyOfRange(itemsInCart, i + 1,
					itemsInCart.length);
			itemSets.addAll(generateCandidates(curr, another));
		}

		// Sort list in order: first long then short combinations
		Collections.sort(itemSets, new Comparator<Integer[]>() {

			@Override
			public int compare(Integer[] o1, Integer[] o2) {
				return o2.length - o1.length;
			}
		});

		// Now we need to fetch "num" items which are not in "itemsInCart"
		// We start with long item sets

		// Get hashSet from Array for fast contains method
		Set<Integer> itemsInCartSet = new HashSet<Integer>();
		for (Integer id : itemsInCart) {
			itemsInCartSet.add(id);
		}

		Integer[] recs = new Integer[num];
		Set<Integer> foundItems = new HashSet<Integer>();
		int gotRecs = 0;
		AssocRulesDAO rulesDAO = new AssocRulesDAOImpl(conn);

		Iterator<Integer[]> it = itemSets.iterator();
		outer: while ((gotRecs < num) && (it.hasNext())) {

			Integer[] currItemSet = it.next();
			List<String> conseqs = rulesDAO
					.getConsequentsByAntecedent(RuleUtils
							.idsArrayToString(currItemSet));

			// For each consequent from db for current itemSet
			for (String consequent : conseqs) {
				Integer[] ids = RuleUtils.parseRuleIdsList(consequent);

				for (Integer id : ids) {
					if (!itemsInCartSet.contains(id)
							&& !foundItems.contains(id)) {
						recs[gotRecs] = id;
						foundItems.add(id);
						gotRecs++;

						if (gotRecs == num) {
							break outer;
						}
					}
				}
			}

		}
		DBUtils.close(conn);
		return recs;
	}

	private List<Integer[]> generateCandidates(Integer[] curr, Integer[] another) {
		List<Integer[]> candidates = new ArrayList<Integer[]>();
		candidates.add(curr);
		if (another.length != 0) {

			for (int i = 0; i < another.length; i++) {
				Integer[] newCand = Arrays.copyOf(curr, curr.length + 1);
				newCand[newCand.length - 1] = another[i];

				Integer[] newAnother = Arrays.copyOfRange(another, i + 1,
						another.length);
				candidates.addAll(generateCandidates(newCand, newAnother));
			}
		}
		return candidates;
	}
}